import logging
from datetime import date
from decimal import Decimal
from typing import TypedDict, Literal, cast
from django.db.models import Sum, Max, Case, When, F, IntegerField
from django.db import transaction
from django.http import JsonResponse
from administrators.models import Userinfo
from inventory.models import Material, Handle, Record, MaterialGroup
from utils.enums import SCE
from utils.exceptions import BusinessException
from utils.result import R

logger = logging.getLogger('inventory')


# 类型集中区域

# 从前端返回的出入库单，数据部分
class RecordList(TypedDict):
    index: int  # 序号，表明在列表中的位置
    material_id: int
    whole_piece: int
    scatt_piece: int
    weight: int
    remark: str


class Type2(TypedDict):
    index: int  # 序号，从零开始，表示该条记录在列表之中的下标
    whole_piece: int
    scatt_piece: int
    weight: Decimal
    remark: str


class Type1(TypedDict):
    actual_date: date
    total: int


# 物料类
class M(object):
    def __init__(self, material_obj: Material):
        self.material_obj: Material = material_obj
        self.set_piece: int = material_obj.set_piece
        self.theory_scatt_weight: Decimal = material_obj.theory_scatt_weight  # 每支的理论重量
        self.enable: bool = material_obj.enable

        self.group: MaterialGroup = material_obj.group
        self.code: str = material_obj.code
        self.name: str = material_obj.name
        self.model: str = material_obj.model
        self.operate_date: date = material_obj.operate_date
        self.operator: Userinfo = material_obj.operator

        self.weight: Decimal = material_obj.weight
        self.theory_weight: Decimal = material_obj.theory_weight
        self.whole_piece: int = material_obj.whole_piece
        self.scatt_piece: int = material_obj.scatt_piece
        self.latest_date: date = material_obj.latest_date

    # 修改code
    def modify_code(self, code: str) -> bool:
        if Material.objects.filter(code=code).exists():
            return False
        else:
            self.code = code
            return True

    # 修改set_piece
    # 修改成功后，还要改所有仓库中物料的件支数，和该物料下属全部记录的件支数
    def modify_set_piece(self, new_set_piece: int):
        old_set_piece = self.set_piece  # 在修改前保留旧的件支数
        self.set_piece = new_set_piece
        # 修改物料所有仓库的件数和支数
        temp1, temp2 = self.modify_piece_number(self.whole_piece, self.scatt_piece, old_set_piece)
        self.whole_piece = temp1
        self.scatt_piece = temp2
        # 修改该物料下所有记录的件数和支数
        temp3 = Record.objects.filter(material=self.material_obj)
        for k in temp3:
            k.whole_piece, k.scatt_piece = self.modify_piece_number(k.whole_piece, k.scatt_piece, old_set_piece)
            k.save()

    # 得到该物料从某个时间点开始，每天的净增加
    def get_record_list(self, date_point: date) -> tuple[int, list[Type1]]:
        # 获得指定日期前一天的库存数量
        # {'total_inventory': 10}
        result1: dict[str, int] = (Record.objects
        .filter(actual_date__lt=date_point, material_id=self.material_obj)
        .aggregate(
            total_inventory=Sum(Case(
                # 入库记录加上数值
                When(trend=False, then=(F('whole_piece') * self.set_piece) + F('scatt_piece')),
                # 出库记录减去数值
                When(trend=True, then=-((F('whole_piece') * self.set_piece) + F('scatt_piece'))),
                default=0,
                output_field=IntegerField()
            )
            )
        ))
        if result1['total_inventory'] is None:
            result1['total_inventory'] = 0
        # 获得从指定日期开始的所有出入库记录
        # [{'actual_date': datetime.date(2025, 9, 28), 'net_flow': 3}, ]
        result2 = list(Record.objects.filter(actual_date__gte=date_point, material_id=self.material_obj)
                       .values('actual_date').annotate(total=Sum(
            Case(
                When(trend=False, then=F('whole_piece') * self.set_piece + F('scatt_piece')),
                When(trend=True, then=-(F('whole_piece') * self.set_piece + F('scatt_piece'))),
                default=0,
                output_field=IntegerField()
            ))).exclude(total=0).order_by('actual_date', 'trend'))
        return result1['total_inventory'] , cast(list[Type1], cast(object, result2))

    # 插入一条出库记录，检查历史记录的某天会不会为零。则total_piece必然是负数
    # total_piece 要出入的总支数 入库为正，出库为负 date_point 插入的日期
    def create_check(self, date_point: date, total_piece: int) -> bool:
        # total_piece必然是负的，插入一条出库记录才需要校验合法性
        total_scatt_piece, temp2 = M.get_record_list(self, date_point)
        if not temp2:  # 如果数组为空，意味着这条记录要插入的日期以及以后没有任何记录
            temp2.append({'actual_date': date_point, 'total': total_piece})
        elif date_point != temp2[0]['actual_date']:  # 要插入的数据的日期不存在其他数据
            temp2 = [{'actual_date': date_point, 'total': total_piece}] + temp2
        elif date_point == temp2[0]['actual_date']:  # 要插入的数据的日期存在其他数据
            temp2[0]['total'] += total_piece
        else:
            raise BusinessException(SCE.DB_ERR)
        # 之后每天的
        for i in temp2:
            total_scatt_piece += i['total']
            if total_scatt_piece < 0:
                return False
        return True

    # 删除记录审查，当发现删除的是入库记录时，检查是否存在风险，删除出库记录没有风险
    def delete_check(self, date_point: date, total_piece: int) -> tuple[bool, date] | bool:
        # temp2不可能为空，因为至少包括要删除的记录，且要删除的记录必然被包含在 temp2 中的第一个元素中
        # 并且，被删除的记录的日期就是temp2中第一条记录的日期
        total_scatt_piece, temp2 = M.get_record_list(self, date_point)
        if temp2[0]['actual_date'] == date_point:
            temp2[0]['total'] += total_piece
        else:
            raise BusinessException(SCE.DB_ERR)
        # 进行校验
        for i in temp2:
            total_scatt_piece += i['total']
            if total_scatt_piece < 0:
                return False
        return True

    # 传入旧的件支数，根据新的件支数纠正件数和支数，注意，使用该函数时，必须保证件支数已经被改为新的件支数
    def modify_piece_number(self, whole_piece: int, scatt_piece: int, old_set_piece: int) -> tuple[int, int]:
        return divmod(whole_piece * old_set_piece + scatt_piece, self.set_piece)

    # 根据要增加或减少的件数和支数，调整material
    def piece_number_change(self, whole: int, scatt: int, trend: bool) -> None:
        temp2 = -(whole * self.set_piece + scatt) if trend else whole * self.set_piece + scatt
        temp1 = self.whole_piece * self.set_piece + self.scatt_piece
        temp3 = temp1 + temp2
        temp4, temp5 = divmod(temp3, self.set_piece)
        self.whole_piece = temp4
        self.scatt_piece = temp5

    # 根据最新件数和支数修改所有仓库的理论重量
    def theory_weight_modify(self) -> None:
        weight = (self.whole_piece * self.set_piece + self.scatt_piece) * self.theory_scatt_weight
        self.theory_weight = weight

    # 修正最近日期
    def latest_date_modify(self, record_obj: Record, add_or_delete: Literal['add', 'delete']) -> None:
        # 如果是添加记录，那只需要比较大小就行
        if add_or_delete == 'add':
            if record_obj.actual_date > self.latest_date:
                self.latest_date = record_obj.actual_date
        else:  # 如果是删除记录
            # 如果要删除的记录的日期大于等于最新日期，则需要修正
            if record_obj.actual_date >= self.latest_date:
                max_actual_date = (Record.objects
                                   .filter(material=record_obj.material)
                                   .exclude(id=record_obj.id)
                                   .aggregate(max_actual_date=Max('actual_date')))['max_actual_date']
                # 如果物料只剩要被删除的记录自己，那么就会返回None。那么直接修改为2025-01-01
                if max_actual_date is not None:
                    self.latest_date = max_actual_date
                else:
                    self.latest_date = date(2025, 1, 1)

    # 修改实际重量，weight
    def weight_modify(self, weight: Decimal, trend: bool) -> None:
        if trend:  # 如果是出库
            self.weight -= weight
        else:
            self.weight += weight

    # 添加记录
    def add_record(self, trend: bool, date_point: date, data: Type2, handle: Handle, operator: Userinfo) -> bool:
        # 验证要插入的数据是否合法
        if not trend:
            result = True
        else:
            result = M.create_check(self, date_point, -(data['whole_piece'] * self.set_piece + data['scatt_piece']))
        if result:
            # 在存储前纠正件支关系
            t_whole_piece, t_scatt_piece = self.modify_piece_number(data['whole_piece'], data['scatt_piece'],
                                                                    self.set_piece)
            temp1 = Record.objects.create(
                trend=trend,
                material=self.material_obj,
                whole_piece=t_whole_piece,
                scatt_piece=t_scatt_piece,
                weight=data['weight'],
                remark=data['remark'],
                handle=handle,
                actual_date=date_point,
                operate_date=date.today(),
                operator=operator,
            )
            self.piece_number_change(t_whole_piece, t_scatt_piece, trend)  # 改件数
            self.weight_modify(data['weight'], trend)  # 改实际重量
            self.theory_weight_modify()  # 改理论重量
            self.latest_date_modify(temp1, 'add')  # 改最近操作日期
            return True
        else:
            return False

    # 删除记录
    def delete_record(self, record_obj: Record) -> bool | tuple[bool, date]:
        total_piece = record_obj.whole_piece * self.set_piece + record_obj.scatt_piece
        result: tuple[bool, date] | bool
        # 校验是否允许删除
        if record_obj.trend:  # 如果删除的是出库，那必然合法
            result = True
        else:
            result = self.delete_check(record_obj.actual_date, -total_piece)
        if result is True:  # 如果删除合法
            trend = not record_obj.trend
            self.piece_number_change(record_obj.whole_piece, record_obj.scatt_piece, trend)
            self.weight_modify(record_obj.weight, not record_obj.trend)  # 改实际重量
            self.theory_weight_modify()  # 改理论重量
            self.latest_date_modify(record_obj, 'delete')
            record_obj.delete()
            return True
        else:
            return False

    # 完成全部运算，保存到数据库中
    def save_obj(self) -> None:
        self.material_obj.set_piece = self.set_piece
        self.material_obj.theory_scatt_weight = self.theory_scatt_weight  # 每支的理论重量
        self.material_obj.enable = self.enable

        self.material_obj.group = self.group
        self.material_obj.code = self.code
        self.material_obj.name = self.name
        self.material_obj.model = self.model
        self.material_obj.operate_date = self.operate_date
        self.material_obj.operator = self.operator

        self.material_obj.weight = self.weight
        self.material_obj.theory_weight = self.theory_weight
        self.material_obj.whole_piece = self.whole_piece
        self.material_obj.scatt_piece = self.scatt_piece
        self.material_obj.latest_date = self.latest_date

        self.material_obj.save()


# 逐条处理出入库请求函数
def handle_in_out(data: Type2, handle_obj: Handle, user_obj: Userinfo, trend: bool, actual_date: date,
                  material_obj: Material) -> bool:
    obj = M(material_obj)
    if obj.add_record(trend, actual_date, data, handle_obj, user_obj):
        obj.save_obj()
        return True
    else:
        return False


# 添加出入库记录
def record_create(in_out_list: list[RecordList], handle_obj: Handle, actual_date: date, trend: bool,
                  user_obj: Userinfo):
    try:
        if not handle_obj.enable:  # 检查处理方是否处于关闭状态
            return R.error(SCE.no_enum, '处理方式处于关闭状态，请选择别的处理方').data()
        # 开始逐条处理出入库请求
        with transaction.atomic():
            for index, i in enumerate(in_out_list):
                # 对数据格式进行转换
                material_obj = Material.objects.get(id=i['material_id'])
                # 如果传回来的件支重量中有空字符串，一律改为0
                if i['whole_piece'] == '':
                    i['whole_piece'] = 0
                if i['scatt_piece'] == '':
                    i['scatt_piece'] = 0
                if i['weight'] == '':
                    i['weight'] = 0
                temp1: Type2 = {'index': i['index'],
                                'whole_piece': int(i['whole_piece']),
                                'scatt_piece': int(i['scatt_piece']),
                                'weight': Decimal(str(i['weight'])).quantize(Decimal('0.001')),
                                'remark': i['remark']}
                if temp1['whole_piece'] == 0 and temp1['scatt_piece'] == 0:
                    raise BusinessException(SCE.E1, f'第{index + 1}条记录件数和支数都为0')
                if not handle_in_out(temp1, handle_obj, user_obj, trend, actual_date, material_obj):
                    raise BusinessException(SCE.E2, f'第{index + 1}条记录将导致库存不合法，请检查')
            return R.success('提交成功').data()
    except Material.DoesNotExist:
        return R.success('error').data({'index': index, 'msg': f'请重新选择第{index + 1}条记录的物料'})
    except BusinessException as e:
        if e.code == 2:
            return R.success('error').data({'index': index, 'msg': f'第{index + 1}条记录将导致库存不合法，请检查'})
        if e.code == 1:
            return R.success('error').data({'index': index, 'msg': f'第{index + 1}条记录件数和支数都为0'})


# 删除出入库记录
def record_delete(record_obj: Record, user_obj: Userinfo) -> JsonResponse:
    material_obj: Material = record_obj.material
    obj = M(material_obj)
    result = obj.delete_record(record_obj)
    if result is True:
        obj.save_obj()
        return R.success('记录已经删除').data()
    else:
        return R.error(SCE.no_enum, f'删除该记录后出现库存为零的情况').data()


# 修改正品库出入库记录，这里不允许修改日期
# 原理：比较修改前和修改后的差距，整理出一条虚拟的需要插入的记录，看是否合法
def record_update(record_obj: Record, whole_piece: int, scatt_piece: int, weight: Decimal, remark: str,
                  trend: bool) -> bool | tuple[bool, date]:
    t_set_piece: int = record_obj.material.set_piece
    material_obj: Material = record_obj.material
    obj: M = M(material_obj)

    t_total_scatt: int  # 支数在原来的基础上要变动的数目
    t_weight: Decimal  # 实际重量...
    t_trend: bool  # 数目的进或出的方向

    t_whole_piece = record_obj.whole_piece
    t_scatt_piece = record_obj.scatt_piece
    total_new_scatt = whole_piece * t_set_piece + scatt_piece
    total_old_scatt = t_whole_piece * t_set_piece + t_scatt_piece

    # 探讨在不同情况下，件数，支数，实际重量的变化情况
    if record_obj.trend != trend:  # 出入库方向发生了变化
        if record_obj.trend is True and trend is False:
            t_trend = False
        else:
            t_trend = True
        t_total_scatt = total_new_scatt + total_old_scatt
    else:  # 出入库方向没有发生变化
        if record_obj.trend:  # 方向为出
            if total_new_scatt < total_old_scatt:
                t_trend = False
            else:
                t_trend = True
        else:
            if total_new_scatt < total_old_scatt:
                t_trend = True
            else:
                t_trend = False
        t_total_scatt = abs(total_old_scatt - total_new_scatt)

    # 如果总支数发生变化，进行修改
    if total_new_scatt != total_old_scatt or record_obj.trend != trend:
        # 先进行校验
        if record_obj.trend is True and trend is False:  # 由出库变入库
            pass
        elif record_obj.trend is False and trend is False and total_new_scatt > total_old_scatt:  # 入的比原来多
            pass
        elif record_obj.trend is True and trend is True and total_new_scatt < total_old_scatt:  # 出的比原来少
            pass
        else:
            result: tuple[int, list[Type1]] = obj.get_record_list(record_obj.actual_date)
            point_total_scatt = result[0]
            record_list = result[1]
            # 确认要减少的数量
            if record_obj.trend is False and trend is True:
                temp1 = total_new_scatt + total_old_scatt
            else:
                temp1 = abs(total_old_scatt - total_new_scatt)
            for i in record_list:
                point_total_scatt += i['total']
                if point_total_scatt < temp1:
                    return False, i['actual_date']
        # 修改material的件支数
        obj.piece_number_change(0, t_total_scatt, t_trend)

    # 如果重量发生变化
    if record_obj.trend != trend:  # 出入库方向发生了变化
        if record_obj.trend is True and trend is False:
            t_trend = False
        else:
            t_trend = True
        t_weight = record_obj.weight + weight
    else:  # 出入库方向没有发生变化
        if record_obj.trend:  # 如果是出库
            if record_obj.weight < weight:
                t_trend = True
            else:
                t_trend = False
        else:
            if record_obj.weight < weight:
                t_trend = False
            else:
                t_trend = True
        t_weight = abs(record_obj.weight - weight)

    if weight != record_obj.weight or record_obj.trend != trend:
        # 修改material的实际重量
        obj.weight_modify(t_weight, t_trend)

    # 确认修改完后欧结果合法，开始进行修改
    # 先改material
    obj.theory_weight_modify()
    obj.save_obj()
    record_obj.whole_piece, record_obj.scatt_piece = divmod(total_new_scatt, t_set_piece)
    record_obj.trend = trend
    record_obj.weight = weight
    record_obj.remark = remark
    record_obj.save()
    return True
